<!DOCTYPE html>
<html>
<head>
    <title>Unit Tests</title>

    <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css" />

    <script type="text/javascript" src="//cdn.jsdelivr.net/g/underscorejs@1.6.0"></script>
    <script type="text/javascript" src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>

  <!-- build:js https://cdn.knightlab.com/libs/timeline3/%(cdn)s/js/timeline-min.js -->
  <script src="/build/js/timeline.js"></script>
  <!-- endbuild -->


    <script type="text/javascript">

      function date_dict(d) {
        return {
          'milliseconds': d.data.date_obj.getMilliseconds(),
          'seconds': d.data.date_obj.getSeconds(),
          'minutes': d.data.date_obj.getMinutes(),
          'hours': d.data.date_obj.getHours(),
          'day': d.data.date_obj.getDate(),
          'month': d.data.date_obj.getMonth(), // 0-11 remember
          'year': d.data.date_obj.getYear()
        }
      }

      function date_parts_match(a, b, parts) {
        a = date_dict(a);
        b = date_dict(b);
        for (var i = 0; i < parts.length; i++) {
          if (a[parts[i]] != b[parts[i]]) return false;
        };
        return true;
      }

      QUnit.module("TL.Util tests",{});
      QUnit.test("TL.Util", function(assert) {
        var test = [
          [0,1],
          [2,3],
          [5,7]
        ];
        assert.equal(TL.Util.maxDepth(test),1,"Max depth 1.")

        var test = [
          [0,5],
          [2,8],
          [9,10]
        ];
        assert.equal(TL.Util.maxDepth(test),2,"Max depth 2.")

        var test = [
          [0,3],
          [5,8],
          [9,10]
        ];
        assert.equal(TL.Util.maxDepth(test),1,"Max depth 1.")

        var test = [
          [0,3],
          [2,8],
          [4,9],
          [15,20]
        ];
        assert.equal(TL.Util.maxDepth(test),2,"Max depth 2.")

        var test = [
          [0,5],
          [2,8],
          [4,9],
          [15,20]
        ];
        assert.equal(TL.Util.maxDepth(test),3,"Max depth 3.")

        var test = [
          [0,5],
          [2,8],
          [10,25],
          [15,20],
          [18,27],
          [24,28],

        ];
        assert.equal(TL.Util.maxDepth(test),3,"Max depth 3.")

        assert.equal(TL.Util.trim(null),'', "null gets zero length string");
        assert.equal(TL.Util.trim(' bob '), 'bob');
        assert.equal(TL.Util.trim('bob '), 'bob');
        assert.equal(TL.Util.trim('bob'), 'bob');
        assert.ok(TL.Util.trim('bob'), "trimming a non-empty string returns a true value")
        assert.ok(!(TL.Util.trim('')), "trimming an empty string returns a false value")
      });

      QUnit.test("TL.Util color tools", function(assert){
         assert.ok(TL.Util.rgbToHex({r: 255, g: 255, b: 255}).match(/^#ffffff$/i), "RGB white is Hex white");
         assert.ok(TL.Util.rgbToHex('rgb(218,112,214)').match(/^#DA70D6$/i), "colors convert correctly from rgb string");
         var rgb = TL.Util.hexToRgb('#DA70D6');
         assert.equal(rgb.r,218,"test hexToRgb red")
         assert.equal(rgb.g,112,"test hexToRgb green")
         assert.equal(rgb.b,214,"test hexToRgb blue")
         throws(function(assert) { TL.Util.rgbToHex("malformed string") }, "make sure bad values throw exceptions 1");
         throws(function(assert) { TL.Util.rgbToHex("rgb(218,112)") }, "make sure bad values throw exceptions 2");
         throws(function(assert) { TL.Util.rgbToHex({r: 255, g: 255, c: 255}) }, "make sure bad values throw exceptions 3");
         var zero_test = TL.Util.rgbToHex({r: 0, g: 255, b: 255})
         assert.ok(zero_test.match(/^#00FFFF$/i), "make sure a zero value doesn't throw an exception")
         var from_name = TL.Util.hexToRgb('bisque');
         var from_hex = TL.Util.hexToRgb('#ffe4c4');
         assert.equal(from_name.r, from_hex.r, "named colors should work too... red")
         assert.equal(from_name.g, from_hex.g, "named colors should work too... green")
         assert.equal(from_name.b, from_hex.b, "named colors should work too... blue")
         assert.equal(TL.Util.colorObjToHex(from_name), TL.Util.colorObjToHex(from_hex), "objects to hexes");
         assert.ok(TL.Util.colorObjToHex(from_name).match(/^#ffe4c4$/i), "objects to hexes")
         assert.ok(TL.Util.colorObjToHex(from_hex).match(/^#ffe4c4$/i), "objects to hexes")
      });

      QUnit.test("TL.Util.linkify", function(assert) {
        var text = "This is some text with a URL in it http://knightlab.northwestern.edu and then some more text";
        var linked = TL.Util.linkify(text);
        assert.ok(linked.startsWith('This is some text with a URL in it <a'), "should start the same and then have a link -> " + linked);
        assert.ok(linked.match(/href=['"]http:\/\/knightlab.northwestern.edu['"]/), "should have an href -> " + linked);
        assert.ok(linked.match(/>knightlab.northwestern.edu<\/a>/), "should have an href " + linked);

        text = "This is some text with www.google.com in it";
        var linked = TL.Util.linkify(text);
        assert.ok(linked.startsWith('This is some text with <a'), "should start the same and then have a link " + linked);
        assert.ok(linked.match(/href=['"]http:\/\/www.google.com['"]/), "should have an href");
        assert.ok(linked.match(/>www.google.com<\/a>/), "should have the right link text");

        text = "This is some text with support@knightlab.zendesk.com in it";
        var linked = TL.Util.linkify(text);
        assert.ok(linked.startsWith('This is some text with <a'), "should start the same and then have a link " + linked);
        assert.ok(linked.match(/href=['"]mailto:support@knightlab.zendesk.com['"]/), "should have an href " + linked);
        assert.ok(linked.match(/>support@knightlab.zendesk.com<\/a>/), "should have the right link text " + linked);

        text = "This is text which already has <a href='http://google.com'>a link</a> in it."
        var not_linked = TL.Util.linkify(text);
        assert.equal(not_linked,text,'linkify should not have changed anything.')

        text = 'This is text which already has <a href="http://google.com">a link</a> in it.'
        var not_linked = TL.Util.linkify(text);
        assert.equal(not_linked,text,'linkify should not have changed anything.')

        text = 'This is text which already has <a href=http://google.com>a link</a> in it.'
        var not_linked = TL.Util.linkify(text);
        assert.equal(not_linked,text,'linkify should not have changed anything.')

      })

      QUnit.test("TL.Util.findNextGreater", function(assert) {
        var l = [1, 5, 10, 20, 35];
        assert.equal(TL.Util.findNextGreater(l,1),5, "5 is next greatest after 1");
        assert.equal(TL.Util.findNextGreater(l,5),10, "10 is next greatest after 5");
        assert.equal(TL.Util.findNextGreater(l,10),20, "20 is next greatest after 10");
        assert.equal(TL.Util.findNextGreater(l,15),20, "correctly handle a curr val which isn't in the list");
        assert.equal(TL.Util.findNextGreater(l,35),35, "handle value at end of list");
        assert.equal(TL.Util.findNextGreater(l,40),40, "handle value greater than max in list");
        assert.equal(TL.Util.findNextGreater(l,40, 35),35, "handle greater than max in list with default");

        assert.equal(TL.Util.findNextLesser(l,1),1, "1 is least");
        assert.equal(TL.Util.findNextLesser(l,5),1, "1 is least after 5");
        assert.equal(TL.Util.findNextLesser(l,10),5, "20 is next greatest after 10");
        assert.equal(TL.Util.findNextLesser(l,15),10, "10 is less than 15 (which isn't in list)");
        assert.equal(TL.Util.findNextLesser(l,35),20, "20 less than 35");
        assert.equal(TL.Util.findNextLesser(l,40),35, "35 is less than 40");
        assert.equal(TL.Util.findNextLesser(l,0, 0),0, "handle less than min in list without default");
        assert.equal(TL.Util.findNextLesser(l,0, -10),-10, "handle less than min in list with default");
      })

      QUnit.test("TL.Util.isEmptyObject", function(assert) {
          o = {}
          assert.ok(TL.Util.isEmptyObject(o),"no keys should be empty");
          o.foo = "  ";
          assert.ok(TL.Util.isEmptyObject(o),"empty string should be empty");
          o.bar = "";
          assert.ok(TL.Util.isEmptyObject(o),"2 empty strings should be empty");
          o.baz = null;
          assert.ok(TL.Util.isEmptyObject(o),"add null, still should be empty");
          o.foo = "  not empty  ";
          assert.equal(TL.Util.isEmptyObject(o), false, "adding a string ")
      })

      QUnit.test("TL.Util.parseYouTubeTime", function(assert) {
          assert.equal(TL.Util.parseYouTubeTime('5s'),5,"parse seconds only")
          assert.equal(TL.Util.parseYouTubeTime('1m5s'),65,"parse m/s")
          assert.equal(TL.Util.parseYouTubeTime('2h4m5s'),7445,"parse h/m/s")
          assert.equal(TL.Util.parseYouTubeTime('5m'),300,"parse minutes only")
          assert.equal(TL.Util.parseYouTubeTime('1h'),3600,"parse hours only")
          assert.equal(TL.Util.parseYouTubeTime(''),0,"handle empty string")
          assert.equal(TL.Util.parseYouTubeTime(null),0,"handle empty string")
          assert.equal(TL.Util.parseYouTubeTime('4:55'),0,"handle malformed string")
          assert.equal(TL.Util.parseYouTubeTime(5),5,"handle number")
      });

      QUnit.test("TL.Util.ensureUniqueKey", function(assert) {
          var o = { foo: 1, bar: 2 }
          assert.ok(!(TL.Util.ensureUniqueKey(o, 'foo') in o), "should be unique");
          assert.equal(TL.Util.ensureUniqueKey(o, 'baz'), 'baz', "not in there, give it back");
          assert.equal(TL.Util.ensureUniqueKey(o, 'foo'), 'foo-2', "treat existing as 1-based");
          o['foo-2'] = 3;
          assert.ok(!(TL.Util.ensureUniqueKey(o, 'foo') in o), "should be unique");
          var random = TL.Util.ensureUniqueKey(o, '');
          assert.ok(TL.Util.trim(random), "Should get a non-empty string");
          assert.ok(!(random in o), "empty string should get non-empty unique");
      });


      QUnit.module("TL.Date tests",{});

      QUnit.test("TL.Date", function(assert) {
        var refdate = TL.Date.makeDate({year: 2000, month: 6, day: 21});
        var jsdate = new Date();
        throws(function(assert) { refdate.isBefore(jsdate) }, "Don't allow comparison with JS Dates");
        jsdate = TL.Date.makeDate(new Date());
        assert.ok(refdate.isBefore(jsdate),"refdate should be before any new date");
        assert.ok(jsdate.isAfter(refdate),"any new date should be after refdate");

        var smalldate = TL.Date.makeDate({year: 2015});
        assert.ok(!isNaN(smalldate.getTime()),"small dates are human");

        var bigdate = TL.Date.makeDate({year: 1000000});
        assert.ok(!isNaN(bigdate.getTime()),"big dates are cosmological");

        var cdate = TL.Date.makeDate({year: 2014, display_date: "hello"});
        assert.equal(cdate.getDisplayDate(), 'hello', 'display_text overrides other formatting')

        var date = TL.Date.makeDate({year: 75});
        assert.equal(date.getDisplayDate(), "75", "handle years in the first century CE correctly")

      });

      QUnit.test("TL.Date.parseDate", function(assert) {
        var d = TL.Date.parseDate('2014-08-20');
        assert.equal(d.year,'2014','three part date gets year 2014');
        assert.equal(d.month,'08', 'three part date gets month 08');
        assert.equal(d.day,'20', 'three part date gets day 20');

        var d = TL.Date.parseDate('2014-08');
        assert.equal(d.year,'2014','two part date gets year 2014');
        assert.equal(d.month,'08','two part date gets month 08');
        assert.ok(!(d.day), 'two-part date has no day');

        var d = TL.Date.parseDate('2014');
        assert.equal(d.year,'2014','year-only date gets 2014');
        assert.ok(!(d.month), 'year only date has no month');
        assert.ok(!(d.day), 'year only date has no day');

        var d = TL.Date.parseDate('-6');
        assert.equal(d.year,'-6','year-only date gets -6 (BC ok)');
        assert.ok(!(d.month), 'year only date has no month');
        assert.ok(!(d.day), 'year only date has no day');


      });

      QUnit.test("TL.Date.findBestFormat", function(assert) {
        var date = new TL.Date({'year': 1975, month: 7})
        assert.equal(date.findBestFormat(),"month","Expect a format key")
        assert.equal(date.findBestFormat(true),"month_short","Expect a different format key for short (legacy)")
        assert.equal(date.findBestFormat('short'),"month_short","Expect a different format key for short (explicit)")

      });

      QUnit.test("TL.Date.floor", function(assert) {
          throws(function(assert) { TL.Date.makeDate(new Date()).floor('foobar'); },new RegExp(/invalid_scale_err/),"bad scale throws invalid_scale_err error.")


          var d = TL.Date.makeDate(new Date(1407440158306)); // Thu Aug 07 2014 14:35:58 GMT-0500 (CDT)
          var floored = d.floor('millisecond');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month','date','hours','minutes','seconds','milliseconds']), 'millisecond rounding doesnt change others')
          assert.equal(date_obj.getTime(), d.getTime(), 'rounds to millisecond')

          var floored = d.floor('second');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month','date','hours','minutes', 'seconds']), 'second rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'seconds round to 0 millis ' + floored)

          var floored = d.floor('minute');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month','date','hours','minutes']), 'minute rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'minutes round to 0 millis ' + floored)
          assert.equal(date_obj.getSeconds(),0,'minutes round to 0 seconds ' + floored)

          var floored = d.floor('hour');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month','date','hours']), 'hour rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'hours round to 0 millis ' + floored)
          assert.equal(date_obj.getSeconds(),0,'hours round to 0 seconds ' + floored)
          assert.equal(date_obj.getMinutes(),0,'hours round to 0 minutes ' + floored)

          var floored = d.floor('day');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month','date']), 'day rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'days round to 0 millis ' + floored)
          assert.equal(date_obj.getSeconds(),0,'days round to 0 seconds ' + floored)
          assert.equal(date_obj.getMinutes(),0,'days round to 0 minutes ' + floored)
          assert.equal(date_obj.getHours(),0,'days round to 0 hours ' + floored)

          var floored = d.floor('month');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year','month']), 'month rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'months round to 0 millis ' + floored)
          assert.equal(date_obj.getSeconds(),0,'months round to 0 seconds ' + floored)
          assert.equal(date_obj.getMinutes(),0,'months round to 0 minutes ' + floored)
          assert.equal(date_obj.getHours(),0,'months round to 0 hours ' + floored)
          assert.equal(date_obj.getDate(),1,'months round to day 1 ' + floored)

          var floored = d.floor('year');
          var date_obj = floored.data.date_obj;
          assert.ok(date_parts_match(d,floored,['year']), 'year rounding doesnt change others')
          assert.equal(date_obj.getMilliseconds(),0,'years round to 0 millis ' + floored)
          assert.equal(date_obj.getSeconds(),0,'years round to 0 seconds ' + floored)
          assert.equal(date_obj.getMinutes(),0,'years round to 0 minutes ' + floored)
          assert.equal(date_obj.getHours(),0,'years round to 0 hours ' + floored)
          assert.equal(date_obj.getDate(),1,'years round to day 1 ' + floored)
          assert.equal(date_obj.getMonth(),0,'years round to month 0 ' + floored)

          var floored = d.floor('decade');
          var date_obj = floored.data.date_obj;
          assert.equal(date_obj.getYear(),110, "decade should round to 2010 " + floored)

          var floored = d.floor('century');
          var date_obj = floored.data.date_obj;
          assert.equal(date_obj.getYear(),100, "century should round to 2000 " + floored)

          var floored = d.floor('millennium');
          var date_obj = floored.data.date_obj;
          assert.equal(date_obj.getYear(),100, "Should round to 2000 " + floored)

          var early_ce = TL.Date.makeDate(-59149708181438); // 8/14/95 (90 not 1995)
          var floored = early_ce.floor('decade');
          assert.equal(floored.getTime(),-59326970400000,'Early floored dates should not go into the 20th Century')

          var age_scale = TL.Date.makeDate({year: 1500000});
          throws(function(assert) {
            age_scale.floor('month');
          },/invalid_scale_err/,'month not valid scale for cosmo');
          assert.equal(age_scale.floor('age').getTime(),1000000,'Should floor to 1M years');

          var cosmo_year = new TL.BigDate({'year': 1945});
          assert.equal(cosmo_year.floor('decade').getTime(),1940, 'decade should floor 1945 to 1940')

      });

      QUnit.test("TL.Date date formatting", function(assert) {
        var msgs = TL.Language.default;
        var d = TL.Date.makeDate({year: '2014', month: '1', day: '1' })
        assert.equal(d.getDisplayDate(msgs),"January 1, 2014")
        var d = TL.Date.makeDate({year: '2014'})
        assert.equal(d.getDisplayDate(msgs),"2014")
      });

      QUnit.test("TL.Util.isTrue", function(assert) {
        assert.ok(TL.Util.isTrue(true));
        assert.notOk(TL.Util.isTrue(false));
        assert.notOk(TL.Util.isTrue());
        assert.notOk(TL.Util.isTrue(null));
        assert.ok(TL.Util.isTrue('true'));
        assert.ok(TL.Util.isTrue(1));
      })

      QUnit.module("TL.DateUtil tests",{});
      QUnit.test("TL.DateUtil", function(assert) {
        var a = {start_date: TL.Date.makeDate({year: 1999})} // will break when we have start dates
        var b = {start_date: TL.Date.makeDate({year: 2000})}
        var c = {start_date: TL.Date.makeDate({year: 2001})}

        var ary = [c,b,a]
        TL.DateUtil.sortByDate(ary);
        assert.equal(ary[0],a,"A should sort first")
        assert.equal(ary[1],b,"B should sort second")
        assert.equal(ary[2],c,"C should sort third")

        ary = [b,a,c];
        TL.DateUtil.sortByDate(ary);
        assert.equal(ary[0],a,"A should sort first")
        assert.equal(ary[1],b,"B should sort second")
        assert.equal(ary[2],c,"C should sort third")

        ary = [b,c,a]
        TL.DateUtil.sortByDate(ary);
        assert.equal(ary[0],a,"A should sort first")
        assert.equal(ary[1],b,"B should sort second")
        assert.equal(ary[2],c,"C should sort third")


      })

      QUnit.test("TL.DateUtil.parseTime", function(assert){

        var test_date = function(time_str,expected_hour, expected_minute, expected_second, expected_milli) {
          try {
            var parsed = TL.DateUtil.parseTime(time_str);
            assert.equal(parsed.hour,expected_hour,time_str + " expected hour " + expected_hour);
            assert.equal(parsed.minute,expected_minute,time_str + " expected minute " + expected_minute);
            assert.equal(parsed.second,expected_second,time_str + " expected second " + expected_second);
            assert.equal(parsed.millisecond,expected_milli,time_str + " expected milli " + expected_milli);
          } catch(e) {
            assert.ok(false, "Failure testing " + time_str + " got " + e);
          }


        }
        test_date('11:30:40.5', 11, 30, 40, 500);


        test_date('11:30', 11, 30, null, null);
        test_date('11.30', 11, 30, null, null);
        test_date('1130', 11, 30, null, null);
        test_date('11.30pm', 23, 30, null, null);
        test_date('11:30 pm', 23, 30, null, null);
        test_date('1130 pm', 23, 30, null, null);
        test_date('11:30:40', 11, 30, 40, null);
        test_date('23:30:40', 23, 30, 40, null);
        test_date('11:30:40 pm', 23, 30, 40, null);
        test_date('11:30:40pm', 23, 30, 40, null);
        test_date("12:33 PM", 12, 33, null, null);
        test_date("1233", 12, 33, null, null);
        test_date("12:33 a.m.", 0, 33, null, null);
        test_date("0033", 0, 33, null, null);
        throws(function(assert) {TL.DateUtil.parseTime('24:30')}, /invalid_hour_err/i, "hour 24 throws invalid_hour_err error");
        throws(function(assert) {TL.DateUtil.parseTime('23:30pm')}, /invalid_hour_err/i, 'hour 23 with pm throws invalid_hour_err error' );



      });

      QUnit.module("TL.Language tests",{});
      QUnit.test("TL.Language",function(assert) {
        var msgs = new TL.Language();
        assert.equal(msgs.name,"English","English is default");
        assert.equal(msgs.lang,"en","en is default");
        assert.equal(msgs._('loading'),"Loading","get a message");
        assert.equal(msgs._('foobar'),"foobar","invalid keys should pass through");

        var msgs = TL.Language.fallback;
        assert.equal(msgs.name,"English","English is default");
        assert.equal(msgs.lang,"en","en is default");
        assert.equal(msgs._('loading'),"Loading","get a message");
        assert.equal(msgs._('foobar'),"foobar","invalid keys should pass through");

        assert.equal(TL.Language.formatNumber(18.54,'%.1f and some other stuff'), "18.5 and some other stuff", "check rounding and replacement")

        assert.equal(TL.Language.formatNumber(18.54,'a literal display year'), 'a literal display year', "pass through literals as display dates")

        assert.equal(msgs.formatBigYear({'year': 20000}, 'foobar'), '20.0 thousand years ago', "use fallback bigdateformat")
      })

      QUnit.test( "TL.Language test non-english", function(assert) {
          var msgs = new TL.Language({language: 'pl'});
          assert.equal(msgs.name,"English","English is default and polish doesn't have one");
          assert.equal(msgs.lang,"pl","should have gotten 'pl'");
          assert.equal(msgs._('loading'),"\u0141adowanie","get a message for loading");
          assert.equal(msgs._('foobar'),"foobar","invalid keys should pass through");
      });


      QUnit.test("TL.Language date formatting", function(assert) {
        var msgs = TL.Language.fallback;
        var d = new Date(2014,7,7,4,5,0);
        assert.equal(msgs.formatDate(d,'d'),'7','short day');
        assert.equal(msgs.formatDate(d,'dd'),'07','padded day');
        assert.equal(msgs.formatDate(d,'ddd'),'Thurs.','short day of week');
        assert.equal(msgs.formatDate(d,'dddd'),'Thursday','full day of week');
        assert.equal(msgs.formatDate(d,'m'),'8','month number');
        assert.equal(msgs.formatDate(d,'mm'),'08','padded month number');
        assert.equal(msgs.formatDate(d,'mmm'),'Aug.','short month name');
        assert.equal(msgs.formatDate(d,'mmmm'),'August','long month name');
        assert.equal(msgs.formatDate(d,'yy'),'14','short year');
        assert.equal(msgs.formatDate(d,'yyyy'),'2014','long year');
        assert.equal(msgs.formatDate(d,'h'),'4','hour');
        assert.equal(msgs.formatDate(d,'hh'),'04','padded hour');
        assert.equal(msgs.formatDate(d,'H'),'4','hour (24HR)');
        assert.equal(msgs.formatDate(d,'HH'),'04','padded hour (24HR)');
        assert.equal(msgs.formatDate(d,'M'),'5','minute');
        assert.equal(msgs.formatDate(d,'MM'),'05','padded minute');
        var d2 = new Date(d.getTime()+(1000*60*60*12));
        assert.equal(msgs.formatDate(d2,'H'),'16','hour (24HR after noon)');
        assert.equal(msgs.formatDate(d2,'HH'),'16','padded hour (24HR after noon)');
        assert.equal(msgs.formatDate(d,'s'),'0','second');
        assert.equal(msgs.formatDate(d,'ss'),'00','padded second');

        var d = new Date(2014,7,7,4,5,0);
        d.setFullYear(-500);
        assert.equal(msgs.formatDate(d,'yyyy'),'500 <span>BCE</span>','BCE date');


      });

      // QUnit.test("TimeScale._assessGroups", function(assert){
      //   var slides = [{group: 'alice'}, { group: 'bob'}, {group: 'alice'}, {group: 'carol'}];
      //   var groups = TL.TimeScale._assessGroups(slides);
      //   assert.equal(groups.length,3);
      // });


      function asyncJSON(url) {
            var data = TL.ajax({
                url: url,
                async: false
            });
            return JSON.parse(data.responseText);
      }

      QUnit.module("TL.ConfigFactory tests",{});

      QUnit.test("TL.ConfigFactory", function(assert) {
        var data = TL.ConfigFactory.googleFeedJSONtoTimelineJSON(asyncJSON('test/GoogleSpreadsheetFeedLegacyFormat.json'));
        assert.equal(7, data.events.length, "7 entries in GoogleSpreadsheetFeedLegacyFormat");
        assert.ok(data.title, "There should be a title.")
        var data = TL.ConfigFactory.googleFeedJSONtoTimelineJSON(asyncJSON('test/GoogleSpreadsheetFeedTJS3Format.json'));
        assert.equal(7, data.events.length, "7 events in GoogleSpreadsheetFeedTJS3Format (one title)");
        assert.ok(data.title, "There should be a title.")
        assert.equal(2012, data.events[0].start_date.year, "first non-title event is year 2012");
        assert.equal(2011, data.events[6].start_date.year, "last non-title event is year 2011");
      });

      QUnit.test("TL.ConfigFactory.parseGoogleSpreadsheetURL", function(assert) {
        var key = '1cWqQBZCkX9GpzFtxCWHoqFXCHg-ylTVUWlnrdYMzKUI';
        var parts = TL.ConfigFactory.parseGoogleSpreadsheetURL(key);
        assert.equal(key, parts.key, "Bare sheet ID should come back in key")

        var url = 'https://docs.google.com/spreadsheets/d/1a8jYcSMWGXupicLJhtNAhkQfta8Fc5qKinDIJtroOAI/pubhtml'
        var key = '1a8jYcSMWGXupicLJhtNAhkQfta8Fc5qKinDIJtroOAI';
        var parts = TL.ConfigFactory.parseGoogleSpreadsheetURL(url);
        assert.equal(key, parts.key, "new-ish url format should get the right key")

        var url = 'https://docs.google.com/spreadsheets/d/1a8jYcSMWGXupicLJhtNAhkQfta8Fc5qKinDIJtroOAI/pubhtml?gid=2066744085'
        var key = '1a8jYcSMWGXupicLJhtNAhkQfta8Fc5qKinDIJtroOAI';
        var worksheet = '2066744085';
        var parts = TL.ConfigFactory.parseGoogleSpreadsheetURL(url);
        assert.equal(key, parts.key, "new-ish url format should get the right key")
        assert.equal(worksheet, parts.worksheet, "new-ish url format should get the right worksheet")

        var url = 'https://docs.google.com/spreadsheets/d/1_7l1RsxQIodkOuKguCPMVQWgmQLGoPr7nBQa9l1k5_4/pubhtml'
        var key = '1_7l1RsxQIodkOuKguCPMVQWgmQLGoPr7nBQa9l1k5_4';
        var parts = TL.ConfigFactory.parseGoogleSpreadsheetURL(url);
        assert.equal(key, parts.key, "new-ish url format should get the right key")
      });

      QUnit.module("TimelineConfig ajax tests", {});

      QUnit.test("Handling CDN 'preview' URLs", function (assert){
        assert.expect(2);
        var bad_url = 'https://cdn.knightlab.com/libs/timeline3/latest/embed/index.html?source=1cWqQBZCkX9GpzFtxCWHoqFXCHg-ylTVUWlnrdYMzKUI&font=Default&lang=en&initial_zoom=2&height=650';
        var done = assert.async();
        var config = TL.ConfigFactory.makeConfig(bad_url,function(config) {
            assert.notOk(config.isValid(), "Bad URL should result in invalid timeline config.");
            var tl_error = config.messages.errors[0];
            assert.equal(tl_error.message_key, 'invalid_url_err', "Expect 'invalid_url_err' in message key.");
            done();
        })
      })

      QUnit.module("user-interface JSON tests", {});

      QUnit.test("TimelineConfig for user-interface", function(assert) {

        var done = assert.async();
        TL.ConfigFactory.makeConfig('/examples/user-interface/timeline3.json',function(config){
            assert.ok((typeof(config) != undefined) ,"config loaded")
            console.log(config," is null? ")
            window.stash = config;
            assert.ok((typeof(config.events) != undefined ),"config has events")
            var slides = config.events;
            var sorted = true;
            for (var i = 0; i < slides.length - 1; i++) {
              var earlier = slides[i];
              var later = slides[i+1];
              sorted = sorted && (earlier.start_date.data.date_obj <= later.start_date.data.date_obj);
            };
            assert.ok(sorted,"dates are sorted");
            done();
        });

      });



      QUnit.test("TimeScale", function(assert) {
          var done = assert.async();
          TL.ConfigFactory.makeConfig('/examples/user-interface/timeline3.json',function(config){
              assert.ok(config,"test data loaded");
              var timescale = new TL.TimeScale(config,{display_width: 2200, screen_multiplier: 3});
              assert.equal(timescale.getMinorScale(),'year',"timescale should be years");
              assert.ok(timescale.getTicks().major.ticks.length > 0,"should be some major ticks")
              assert.ok(timescale.getTicks().minor.ticks.length > 0,"should be some minor ticks")
              assert.equal(timescale.getNumberOfRows(),3,"Expecting 3 rows") // this will change if someone monkeys around with the config file!
              assert.equal(timescale.getScale(),'human','test case uses human dates')
              done();
          });


      });


      QUnit.module("marktwain.json tests",{});

      QUnit.test("TimeScale", function(assert) {
          var done = assert.async();
          TL.ConfigFactory.makeConfig('/examples/twain/marktwain.json',function(config){
              var timescale = new TL.TimeScale(config);
              assert.equal(timescale.getNumberOfRows(),5, "expect 5 rows");
              assert.equal(timescale._positions.length,config.events.length,"There should be the same number of positions (" + timescale._positions.length + ") as slides ("+config.events.length+")")
              var row_check = new Array(timescale.getNumberOfRows());
              for (var i = 0; i < row_check.length; i++) {
                row_check[i] = [];
              };
              var all_posinfos_defined = true;

              for (var i = 0; i < config.events.length; i++) {
                var pos_info = timescale.getPositionInfo(i);
                all_posinfos_defined = all_posinfos_defined && (typeof(pos_info) != "undefined")
                row_check[pos_info.row].push(pos_info);
              };
              assert.ok(all_posinfos_defined,"There should be a pos_info for every idx");
              var slides_in_rows = 0;
              for (var i = 0; i < timescale.getNumberOfRows(); i++) {
                slides_in_rows += row_check[i].length;
                var no_overlaps = true;
                for (var j = 1; j < row_check[i].length; j++) {
                  no_overlaps = no_overlaps && (row_check[i][j-1].end < row_check[i][j].start);
                };
                assert.ok(no_overlaps,"No overlaps in row " + i)
              };
              assert.equal(slides_in_rows,config.events.length,"In checking rows we should have seen all slides")
              done();
          });
      })

      QUnit.test("TimeScale max rows", function(assert) {
        var config = new TL.TimelineConfig({ scale: 'cosmological',
          events:
          [
            {start_date: {year: 1}, end_date: {year: 1000}},
            {start_date: {year: 100}, end_date: {year: 1500}},
            {start_date: {year: 200}, end_date: {year: 2000}},
            {start_date: {year: 300}, end_date: {year: 2500}},
            {start_date: {year: 400}, end_date: {year: 2600}},
            {start_date: {year: 500}, end_date: {year: 4000}},
          ]
        })
        var timescale = new TL.TimeScale(config,{display_width: 600, screen_multiplier: 3, max_rows: 4});
        assert.equal(timescale.getNumberOfRows(),4,"Max rows 4 should be honored.")
        var timescale = new TL.TimeScale(config,{display_width: 600, screen_multiplier: 3, max_rows: 3});
        assert.equal(timescale.getNumberOfRows(),3,"Max rows 3 should be honored.")
      });



    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>
